Développement d'applications avec Qt par l'exemple


Vous trouverez à ce sujet la présentation des Travaux Pratiques et le tutoriel Qt.

Le framework Qt est très largement employé dans les applications informatiques standard de type Desktop mais également dans les applications embarquées sur plate-forme mobiles Android, IOS, ou autre2.

Par rapport à d'autres frameworks, Qt présente l'avantage d'être encore libre et open source, de s'exécuter aussi bien sous Windows, Linux et MasOS, et surtout d'avoir des fonctionnalités de cross-compilation directement intégrées à son outil de développement QtCreator.

Dans ce module, nous allons voir les principales fonctionnalités du framework Qt, sachant que la richesse de ces dernières demanderait plusieurs années de travail pour en faire le tour.

Remarque : Pour apprendre d’autrui, il faut se reconnaître dans l’autre et lui ressembler. C’est ce que montrent des chercheurs qui étudient les mécanismes cérébraux et comportementaux de l’apprentissage social. Ils tentent notamment d’identifier les facteurs qui y sont favorables ou qui, au contraire, perturbent l’acquisition des connaissances. Ce document est donc basé sur le principe de l'apprentissage uniquement par l'exemple « regardez comment je fais, tirez en la logique, faites le vous même ».

Vous découvrirez dans ce tutoriel des exemples simples traitant de:

La mise en oeuvre de classes de base de Qt, QString, QPolygon et QPoint

à travers un exemple consistant à manipuler des polygones du plan, c’est à dire

  • les définir à partir de leurs sommets et d’un nom (une chaîne de caractères)
  • les translater d’un vecteur donné
  • les afficher sur la console, ex: P1 = (0,0), (0,10), (10,10), (0,0),
  • calculer leur aire
  • calculer leur intersection avec un autre polygone
  • vérifier si un point donné fait partie du polygone ou non

ouvrir le projet dans : classe-heritee-poygon-console

La classe QFile pour générer des fichiers CSV

Il est parfois bien utile de sauver des données dans un fichier pour les réutiliser plus tard. Dans Qt, il existe une classe QFile qui encapsule toutes les fonctionnalités liées à la gestion de ficher (texte ou binaire).

Dans l’exemple, nous écrivons les échantillons d’une sinusoïde dans un fichier CSV (comma separated values), puis nous les relisons pour les imprimer sur l’écran.

Vous pouvez vérifier le contenu du fichier avec openOffice et même en tracer la courbe.

ouvrir le projet dans : fichier-texte-csv

La classe QFile avec un flux QTextStream

Pour faire exactement la même chose que précédemment, on peut également utiliser un flux texte, QTextStream, pour avoir accès aux redirections.

ouvrir le projet dans : fichier-texte-csv-flux

L'écriture et la lecture de fichiers binaires

Quand on a beaucoup de données, il est préférable d’utiliser des fichiers binaires plutôt que des fichiers texte. (1 octet de donnée = 1 octet sauvé)

La classe QFile permet également de gérer ce format de données.

ouvrir le projet dans : fichier-binaire

La classe QImage pour manipuler des images

Si on vous demandait d’écrire un programme qui :

  1. Ouvre une image à partir d’un fichier jpeg ou autre (bmp, png, .etc.).

  2. Échange les plans rouge et vert puis sauve le résultats

  3. Transforme l’image en N&B et sauve le résultats

  4. remplace le fond noir de l’image précédente par du vert et sauve le résultats

bien entendu, vous n’avez que 10 mn pour faire ça. Vous seriez bien embêté !

Avec la classe QImage, les développeurs de Qt ont prévu toutes les actions de base sur des images: enregistrer, charger, accéder aux pixels, convertir les formats, .etc.

ouvrir le projet dans : classe-qimage-console

La classe QDir pour accéder aux répertoires du système de fichier

Et si on demandait maintenant de convertir toutes les images JPEG d’un répertoire source, spécifié en 1 er argument de la ligne de commande, en images BMP dans un répertoire destination, spécifié en 2ème argument de la ligne de commande.

Si le répertoire source n’est pas spécifié, le programme affiche un texte d’aide Si le répertoire destination n’est pas spécifié, il sauve les images dans un répertoire nommé converted sous le répertoire destination.

Nota : Les images JPEG peuvent avoir l’extension .jpg, .jpeg, .JPG ou .JPEG.

ouvrir le projet dans : convertisseur-images-repertoires

Les applications GUI

Jusqu’à présent, nous nous sommes contentés d’écrire des applications « console » comme on en faisait dans les années 80 (ça doit être dû à l’age avancé du prof ... ).

Nous allons maintenant mettre en œuvre des applications graphiques présentant une interface dans une fenêtre graphique. C’est quand même plus joli !

Placement manuel de composants sur un QWidget La classe QWidget encapsule toutes les fonctionnalités d’un composant visible. Cette classe est, entre autre, la classe mère 3 de tous les autres composants (QPushButton, QLineEdit, QSpinBox, QMenu, .etc.)

Si on instancie un QWidget avec son constructeur par défaut et qu’on l’affiche (méthode show), on obtient une fenêtre graphique vide, mais qui a déjà tous les comportement des fenêtres graphiques (réduction, redimensionnement, déplacement, .etc.)

Il est possible d’insérer d’autres composants sur cette interface pour réaliser une application. Vous découvrirez au passage un minuscule échantillon de composants graphiques et leur comportements respectifs ainsi qu'une première introduction au processus de communication entre-classes prévu par Qt, les "SIGNAL/SLOT".

ouvrir le projet dans : widget-placement-manuel-composants

Les applications GUI construites avec QtDesigner

Comme vous avez pu le remarquer dans l’exemple précédent, il n’est pas très commode de placer des composants sur l’interface par programme.

Les développeurs de Qt ont donc envisagé une autre solution consistant à utiliser un designer, QtDesigner, pour décrire l’interface de façon interactive, « avec la souris », ainsi que son comportement : connexion de signaux à des slots.

Cette interface est alors entièrement décrite dans un fichier au format XML.

Lors du processus de construction de l’application, les outils Qt se chargent de générer tout le code source nécessaire pour construire l’interface, telle que vous l’avez décrite avec le Designer. (super!)

ouvrir le projet dans : application-widget-qt-designer

Les dialogues standards

Le framework Qt possède une multitude de boites de dialogue prédéfinis couvrant la majorité des opérations d’échange avec l’utilisateur : saisie d’un nombre, d’une chaîne de caractère, question, alerte, navigation dans le système de fichier, .etc.

ouvrir le projet dans : tous-les-dialogues

La gestion dynamique de l’interface - Layout

Jusqu’à maintenant, la taille et la disposition des composants que l’on a placé sur l’interface ne dépendait pas de la taille de la fenêtre principale. Cela peut être très ennuyeux si on exécute le programme sur des machines différentes, n’ayant pas la même résolution et disposition d’écran; exemple : les Desktops ont un écran plus large que haut, les smartphones c’est l’inverse.

Qt dispose d’un ensemble de conteneurs spécifiques, les QLayout, permettant de pallier à ce problème.

Sur le Designer, on les retrouve en haut de la barre latérale :

ouvrir le projet dans : Utilisation-des-Layout-pour-construire-l-interface

La mise en ressource d’images, ou d'autres données

Pour régler le problème de l'exemple précédent, nécessité de mettre les image dans le répertoire d'exécution, une solution consiste à intégrer les images à l’exécutable par le biais des ressources. Ainsi, toute donnée mise en ressource est convertie en fichier de code source et intégré à l'application lors de la compilation. Les ressources peuvent contenir des images, des sons, mais également tous fichiers quelque soient leurs structures (texte ou binaire).

  • Créer un fichier de ressources (menu Nouveau, puis Qt, et enfin, Fichier de ressources)

  • Insérer les images dans le fichier de ressource.

Dans le programme, la ressource est accessible à partir de son chemin et de son nom de fichier précédés par la chaîne ":". Exemple : une image est dans la racine, donnerait ":/image.jpg".

ouvrir le projet dans : Utilisation-des-ressources-pour-accéder-à-des-images

Le dessin sur l’interface et la gestion des événements souris

Pour dessiner directement sur l’interface d’un QWidget (fenêtre graphique), il faut redéfinir sa méthode paintEvent comme suit, dans la déclaration de la classse (header) :

private slots:
    void paintEvent(QPaintEvent *event);

Dans l’implémentation de la classe, il suffit d’instancier un objet QPainter en lui passant le pointeur this (moi, le widget sur lequel on veut dessiner) comme argument :

void Widget::paintEvent(QPaintEvent *event)
{

QPainter painter(this);
. . .
}

L’objet painter contient alors tout un ensemble de méthodes pour dessiner les lignes, des cercles, des images, changer de crayon (pen), de couleur, .etc.

Pour récupérer les événements de la souris sur un QWidget (fenêtre graphique), il faut redéfinir sa méthode mousePressEvent comme suit, dans la déclaration de la classse (header) :

private slots:
void mousePressEvent ( QMouseEvent * event );

L’argument event de la fonction, du type QMouseEvent *, contient toutes les méthodes permettant de récupérer les événements souris : position du click, de la roulette, état du clavier (ctr, shift, alt, .etc.) lors du click.

ouvrir le projet dans : dessins-sur-l'interface-et-gestion-souris

comment récupérer les événements liés au clavier sur un QWidget (fenêtre graphique)

il faut redéfinir sa méthode keyPressEvent comme suit, dans la déclaration de la classse (header) :

void keyPressEvent(QKeyEvent * event);

Un peu comme nous l’avons fait pour la souris, et le dessin.

Les paramètres non volatiles, QSettings

Il est parfois utile qu’un programme garde en mémoire, d’une exécution à l’autre, les valeurs de certains de ses paramètres comme un identifiant, un mot de passe, un chemin, une adresse IP, .etc.

La première solution à ce problème est de les enregistrer dans un fichier avec une structure que vous choisirez et de les relire à chaque démarrage du programme.

Maintenant que vous commencez à bien connaître Qt, vous imaginez bien que les développeurs ont prévu cette situation.

La classe QSettings permet de gérer cela très simplement.

ouvrir le projet dans : parametres-non-volatiles-settings

le compiler et l’exécuter 2 fois pour voir le résultat.

La mise en œuvre de timer, QTimer

Pour qu’un programme fasse des actions en continu, comme une animation graphique par exemple, la première idée qui vient à l’esprit consiste à mettre ses actions dans une boucle sans fin. Hélas, cette manière de faire bloque le programme interdisant tous les événements utilisateur (frappe au clavier, action souris, .etc.). Il n’est alors plus possible de rien faire, même pas de l’arrêter ! Le solution à ce problème est de mettre en œuvre un timer.

La classe QTimer fait ça à merveille.

ouvrir le projet dans : utilisation-et-mise-en-oeuvre-de-timer

La génération d’impression ou de document pdf

Le titre est suffisamment explicite :

Un programme qui imprime des états directement sur l’imprimante (avec gestion des paramètres d’impression bien entendu) ou un programme qui génère des états au format acrobat PDF.

Une classe QPrinter fait ça très facilement.

Pour le pdf, ouvrir le projet dans : utilisation-et-mise-en-oeuvre-de-timer

le compiler et l’exécuter pour voir le résultat. Le fichier se trouve dans le répertoire d'exécution.

Pour l'imprimante, ouvrir le projet dans : impression-d-etats-sur-imprimante

Applications multithread, Qthread

En informatique, le multitâche est une méthode où plusieurs tâches, aussi appelées processus, partagent des ressources de traitement communes comme une unité centrale. Avec un système d'exploitation multitâche, comme Linux ou Windows, vous pouvez lancer plusieurs applications simultanément. Le multitâche fait référence à la capacité d'un système d'exploitation à passer rapidement d'une tâche informatique à l'autre pour donner l'impression que les différentes applications sont en train d'exécuter plusieurs actions simultanément. Le multithread étend l'idée du multitâche aux applications, de sorte que vous pouvez sous-diviser des opérations spécifiques au sein d'une même application en threads individuels. Chaque thread peut fonctionner en parallèle. Le système d'exploitation divise le temps de traitement non seulement entre différentes applications, mais aussi entre chaque thread dans une même application. Un exemple d'application peut être divisé en quatre threads : un thread d'interface utilisateur, un thread d'acquisition de données, une communication en réseau et un thread d'enregistrement. Vous pouvez établir des priorités entre eux afin qu'ils fonctionnent séparément.

Ainsi, dans des applications multithread, plusieurs tâches peuvent s'effectuer en parallèle avec d'autres applications qui fonctionnent sur le système. source,

Applications console :ouvrir le projet dans : utilisation-des threads

le compiler et l’exécuter pour voir le résultat.

Dans une application GUI: ouvrir le projet dans : utilisation-threads-graphique

le compiler et l’exécuter pour voir le résultat.

Applications en réseau, broadcast et capture UDP

Le User Datagram Protocol (UDP) est un des principaux protocoles de télécommunication utilisés par Internet. Il fait partie de la couche transport du modèle OSI, il appartient à la couche 4, comme TCP. Il est détaillé dans la RFC 768 Le rôle de ce protocole est de permettre la transmission de données de manière très simple entre deux entités, chacune étant définie par une adresse IP et un numéro de port. Contrairement au protocole TCP, il fonctionne sans négociation : il n'existe pas de procédure de connexion préalable à l'envoi des données (le handshaking). Donc UDP ne garantit pas la bonne livraison des datagrammes à destination, ni leur ordre d'arrivée. Il est également possible que des datagrammes soient reçus en plusieurs exemplaires. L'intégrité des données est assurée par une somme de contrôle sur l'en-tête. L'utilisation de cette somme est cependant facultative en IPv4 mais obligatoire avec IPv6. Si un hôte n'a pas calculé la somme de contrôle d'un datagramme émis, la valeur de celle-ci est fixée à zéro. La somme de contrôle inclut les adresses IP source et destination. La nature de UDP le rend utile pour transmettre rapidement de petites quantités de données, depuis un serveur vers de nombreux clients ou bien dans des cas où la perte d'un datagramme est moins gênante que l'attente de sa retransmission. Le DNS, la voix sur IP ou les jeux en ligne sont des utilisateurs typiques de ce protocole. source

Un serveur qui broadcast les échantillons d’une sinusoïde sur le port 10001 de la machine locale (localhost) : ouvrir le projet dans : broadcastUDPdatagram

le compiler et l’exécuter pour voir le résultat.

Un client qui capture les datagram ci-dessus, sur la machine locale également, et les affiche sur la console : ouvrir le projet dans : captureUDPdatagram

le compiler et l’exécuter pour voir le résultat.

Application client/serveur TCP

Transmission Control Protocol, abrégé TCP, est un protocole de transport fiable, en mode connecté, documenté dans la RFC 7931 de l’IETF. Dans le modèle Internet, aussi appelé modèle TCP/IP, TCP est situé au-dessus de IP. Dans le modèle OSI, il correspond à la couche transport, intermédiaire de la couche réseau et de la couche session. Les applications transmettent des flux de données sur une connexion réseau. TCP découpe le flux d’octets en segments dont la taille dépend de la MTU du réseau sous-jacent (couche liaison de données).

Un serveur TCP : ouvrir le projet dans : serveur-TCP

le compiler et l’exécuter, puis le tester avec la commande telnet

telnet localhost 10000

tapez X, puis Y, puis autre chose, et enfin Q; vous allez tout suite comprendre son fonctionnement!

Application utilisant des bibliothèques externes

L’utilisation de bibliothèques externes n’est pas tout à fait dans le thème de ce document qui était « le framework Qt », mais j’ai tenu à en parler car l’utilisation conjointe de Qt et d’autres bibliothèques ouvre des horizons infinis. A titre personnel, il m’est arrivé de mettre en œuvre dans une même application Qt plus d’une dizaine de bibliothèques (libusb, libalsa, libespeak, opencv, libdmtx, .etc.).

Utilisation de la bibliothèque libsensors

Pour commencer, j’ai choisi une petite bibliothèque amusante qui permet d’accéder aux capteurs matériels de votre ordinateur: les températures, tensions, vitesses de ventilateurs, .etc.

Installation sous Linux:

sudo apt-get install libsensors4-dev

Fichiers installés :

/usr/include/sensors/error.h
/usr/include/sensors/sensors.h
/usr/lib/x86_64-linux-gnu/libsensors.a
/usr/lib/x86_64-linux-gnu/libsensors.so
/usr/share/doc/libsensors4-dev/changelog.Debian.gz
/usr/share/doc/libsensors4-dev/copyright
/usr/share/doc/libsensors4-dev/libsensors-API.txt.gz
/usr/share/man/man3/libsensors.3.gz

Pour pouvoir lier (linker) avec notre application il faut juste ajouter dans le fichier du projet (.pro) les lignes suivantes permettant de spécifier le chemin d’inclusion des headers (.h) et l’ajout de la bibliothèque libsensors à l’exécutable. Ceci permettra d’ajouter les options à la ligne de commande du compilateur : -I/usr/include/sensors, (-L/usr/lib/x86_64-linux-gnu) et -lsensors

Lignes à rajouter dans le .pro :

##   UTILISATION DE LA BIBLIOTHEQUE libsensors
INCLUDEPATH  += /usr/include/sensors/
LIBS   += -lsensors

Il ne reste plus qu’à consulter la documentation de la bibliothèque, (commande : man libsensors) pour y trouver la liste des fonctions, leurs actions, et leurs prototypes, .etc.

Un exemple permettant d’afficher les valeurs disponibles : ouvrir le projet dans : exemple-libsensors-console;

le compiler et l’exécuter pour voir le résultat.

Le même avec une interface qui se construit en fonction des senseurs disponibles. Un peu plus difficile au niveau Qt ! Dans un premier temps on construit l’interface (dans le constructeur) puis un timer met à jour les valeurs.

ouvrir le projet dans : exemple-libsensors-GUI

le compiler et l’exécuter pour voir le résultat.

Représentation graphique de données avec QWT

La bibliothèque QWT permet de mettre en œuvre des composants visuels de haut niveau tels que : des graphiques avec courbes, des boussoles, des compteurs, des horizons artificiels, .etc. Vous pouvez trouver qwt à : http://qwt.sourceforge.net/ Comme précédemment, il faut tout d’abord installer la bibliothèque. Le problème cette fois-ci est qu’elle n’est pas disponible dans les dépôts de Linux. Il faut donc repartir des codes sources et la recompiler localement. A l’heure où j’écris ce document, la dernière version est la 6.1.3 du 12/06/2016.

Compilation et installation :

  1. Télécharger les codes sources

  2. Décompresser l’archive, dans un répertoire de votre choix (moi j’utilise mon home pour ce genre de choses)

  3. ouvrir le fichier de projet (qwt.pro) avec QtCreator

  4. construire dans un répertoire de votre choix build (sous le répertoire de qwt par exemple). Vous pouvez aller boire un café...

  5. Se rendre dans le répertoire ci-dessus avec un terminal puis entrez la commande :

sudo make install.
  1. La bibliothèque sera installée par défaut sous : /usr/local/qwt-6.1.3

Comme précédemment il faudra rajouter cette bibliothèque dans le fichier de projet :

#   QWT library - must be installed
INCLUDEPATH += /usr/local/qwt-6.1.3/include
LIBS += -L/usr/local/qwt-6.1.3/lib  -lqwt

Pour chercher les headers à /usr/local/qwt-6.1.3/include et pour lier la bibliothèque libqwt.so qui se trouve dans le répertoire /usr/local/qwt-6.1.3/lib

Voilà, il ne reste plus qu’à !

ouvrir le projet dans : qwt-plot-des-belles-courbes

Le compiler et l’exécuter pour voir le résultat.

Vous trouverez sur mon site d’autres exemples :

un horizon artificiel

une boussole

les figures de Lissajou

un compteur de vitesse

Transformée de Fourier avec libfftw

LibFFTW est une bibliothèque de sous-programmes C permettant de calculer la transformée de Fourier discrète (DFT) en une ou plusieurs dimensions, avec une taille d'entrée arbitraire, pour des données réelles et complexes (ainsi que de données paires ou impaires, c'est-à-dire les transformées cosinus / sinus discret, DCT / DST).

Installation : elle est disponible dans les dépôts de Linux en version 3 :

sudo apt-get install libfftw3-dev 

Fichiers installés :

/usr/bin/fftw-wisdom
/usr/bin/fftw-wisdom-to-conf
/usr/bin/fftwf-wisdom
/usr/bin/fftwl-wisdom
/usr/include/fftw3.f
/usr/include/fftw3.f03
/usr/include/fftw3.h
/usr/lib/libfftw3.a
/usr/lib/libfftw3.so
/usr/lib/libfftw3_threads.a
/usr/lib/libfftw3_threads.so
/usr/lib/libfftw3f.a
/usr/lib/libfftw3f.so
/usr/lib/libfftw3f_threads.a
/usr/lib/libfftw3f_threads.so
/usr/lib/libfftw3l.a
/usr/lib/libfftw3l.so
/usr/lib/libfftw3l_threads.a
/usr/lib/libfftw3l_threads.so
/usr/lib/pkgconfig/fftw3.pc
/usr/lib/pkgconfig/fftw3f.pc
/usr/lib/pkgconfig/fftw3l.pc
/usr/share/doc/libfftw3-dev/changelog.Debian.gz
/usr/share/doc/libfftw3-dev/copyright
/usr/share/doc/libfftw3-dev/examples/Makefile.am
/usr/share/doc/libfftw3-dev/examples/Makefile.in
/usr/share/doc/libfftw3-dev/examples/README
/usr/share/doc/libfftw3-dev/examples/bench.c
/usr/share/doc/libfftw3-dev/examples/check.pl
/usr/share/doc/libfftw3-dev/examples/fftw-bench.c
/usr/share/doc/libfftw3-dev/examples/hook.c
/usr/share/man/man1/fftw-wisdom-to-conf.1.gz
/usr/share/man/man1/fftw-wisdom.1.gz
/usr/share/man/man1/fftwf-wisdom.1.gz
/usr/share/man/man1/fftwl-wisdom.1.gz

Le programme d’exemple suivant permet à l’utilisateur de choisir un signal temporel (sinus, carré, pulse ou bruit blanc) puis d’en calculer la transformée de Fourier. Une représentation graphique utilisant la bibliothèque QWT du signal et du spectre est donnée en temps réel. (attention pensez à vérifier que la version de qwt dans le fichier de projet corresponde bien à celle installée sur votre machine)

ouvrir le projet dans : exemple-utilisant-libfftw

le compiler et l’exécuter pour voir le résultat.

Maintenant que vous avez compris la philosophie de Qt, il suffit de jouer avec la complétion de code et avec l'aide de Qt pour aller plus loin.

Le problème à ce stade est plus que d’avoir des idées originales de programmes que de les coder. Inutile de vous poser la question « est-ce que c’est faisable ? », avec Qt, la réponse est oui!

Par contre, si vous avez accroché, le problème récurrent qui se posera à vous sera: « comment trouver le temps pour le faire ? »

24 heures par jour, c’est très court, avec toutes les servitudes que la nature nous impose. Certaines peuvent se remplir en programmant, d'autres hélas non !

Bon courage...